import asyncio
import random
import logging
import os
from ..actions.utils import stream_output
from ..actions.query_processing import plan_research_outline, get_search_results
from ..document import DocumentLoader, OnlineDocumentLoader, LangChainDocumentLoader
from ..utils.enum import ReportSource
from ..utils.logging_config import get_json_handler


"""
`ResearchConductor` 类是 GPT Researcher 项目中的一个核心组件，负责管理和协调研究过程。我来分析一下它的主要功能：

1. **初始化**：
   - 接收一个 researcher 对象
   - 设置日志记录器
   - 获取 JSON 处理器用于记录研究过程

2. **研究计划（plan_research）**：
   - 根据查询和可选的查询领域规划研究
   - 使用搜索引擎获取初步搜索结果
   - 规划研究策略和子任务

3. **执行研究（conduct_research）**：
   - 根据不同的数据源类型执行研究：
     - 指定的源 URL
     - 网络搜索
     - 本地文档
     - 混合模式（网络+本地）
     - Azure 存储
     - LangChain 文档
     - LangChain 向量存储
   - 可选地对源进行筛选和整理

4. **URL 内容获取（_get_context_by_urls）**：
   - 抓取和压缩指定 URL 的内容

5. **向量存储搜索（_get_context_by_vectorstore）**：
   - 通过向量存储搜索相关内容
   - 处理子查询以获得更全面的结果

6. **网络搜索（_get_context_by_web_search）**：
   - 生成子查询
   - 并行处理子查询以提高效率
   - 合并搜索结果

7. **子查询处理（_process_sub_query）**：
   - 处理单个子查询
   - 抓取相关数据
   - 获取与查询相似的内容

8. **向量存储子查询处理（_process_sub_query_with_vectorstore）**：
   - 通过向量存储处理子查询

9. **URL 管理（_get_new_urls）**：
   - 筛选出尚未访问的 URL

10. **相关源 URL 搜索（_search_relevant_source_urls）**：
    - 使用多个检索器搜索相关的 URL

11. **URL 数据抓取（_scrape_data_by_urls）**：
    - 针对子查询搜索相关 URL
    - 抓取这些 URL 的内容

这个类使用了异步编程（asyncio）来并行处理查询和抓取任务，提高效率。它支持多种不同的数据源和检索方法，并提供详细的日志记录和进度反馈。:-)


"""

"""
# ResearchConductor 研究工作流程梳理：以"黑洞"为例

## 初始化阶段
1. 创建 ResearchConductor 实例，接收 researcher 对象
2. 设置日志记录器和 JSON 处理器

## 研究计划阶段 (plan_research)
1. 接收查询"黑洞"
2. 记录日志："规划黑洞的研究"
3. 向用户显示："🌐 正在浏览网络以了解更多关于任务的信息: 黑洞..."
4. 通过 get_search_results 获取初步搜索结果（使用第一个检索器）
5. 向用户显示："🤔 正在规划研究策略和子任务..."
6. 通过 plan_research_outline 生成研究大纲，可能包括：
   - "黑洞的形成过程"
   - "黑洞的结构和特性"
   - "史蒂芬·霍金对黑洞的贡献"
   - "最新的黑洞观测发现"

## 执行研究阶段 (conduct_research)
1. 清除先前访问的 URL 记录
2. 向用户显示："🔍 正在开始研究任务: '黑洞'..."

### 根据数据源类型执行不同的研究流程：

#### 如果使用 Web 搜索（ReportSource.Web.value）：
1. 记录日志："使用网络搜索"
2. 调用 _get_context_by_web_search：
   - 生成子查询列表（如上述大纲内容）
   - 向用户显示："🗂️ 我将基于以下查询进行研究: [子查询列表]..."
   - 并行处理每个子查询（使用 asyncio.gather）：
     
     对于每个子查询（如"黑洞的形成过程"）：
     - 调用 _scrape_data_by_urls：
       - 通过 _search_relevant_source_urls 查找相关 URL（使用多个检索器）
       - 向用户显示："🤔 正在跨多个来源研究相关信息..."
       - 使用 scraper_manager.browse_urls 抓取内容
       - 如有向量存储，将内容加载到向量存储
     - 使用 context_manager 获取与子查询相似的内容
     - 如未找到内容，显示："🤷 没有找到'黑洞的形成过程'的相关内容..."
   
   - 合并所有子查询的结果为一个大的上下文

#### 如果使用向量存储（ReportSource.LangChainVectorStore.value）：
1. 记录日志："使用LangChain向量存储进行研究"
2. 调用 _get_context_by_vectorstore：
   - 生成子查询列表
   - 并行处理每个子查询
   - 使用向量存储查找与每个子查询相关的内容

#### 如果提供了特定 URL（source_urls）：
1. 记录日志："使用提供的源URL"
2. 调用 _get_context_by_urls：
   - 获取未访问过的新 URL
   - 抓取这些 URL 的内容
   - 加载到向量存储（如有）
   - 获取与主查询"黑洞"相似的内容
3. 如未找到内容，显示："🧐 我无法在提供的源中找到相关内容..."
4. 如需补充，执行额外的网络搜索

## 源整理阶段
1. 设置 researcher.context = research_data
2. 如果配置了源整理(curate_sources)：
   - 记录日志："整理源"
   - 使用 source_curator.curate_sources 整理源

## 结束阶段
1. 向用户显示："研究步骤已完成。💸 总研究成本: $X.XX"
2. 更新 JSON 处理器中的成本和上下文信息
3. 记录日志："研究完成。上下文大小: XXXX 字符"
4. 返回研究上下文

## 工作流程中的关键点
1. **异步并行处理**：通过 asyncio.gather 并行处理多个子查询，提高效率
2. **多源检索**：使用多个检索器查找信息
3. **去重机制**：通过 visited_urls 集合避免重复处理 URL
4. **多数据源支持**：支持多种数据源（网络、本地文档、混合、Azure、LangChain等）
5. **进度反馈**：通过 stream_output 向用户实时显示研究进度
6. **源整理**：可选地对信息源进行筛选和整理
7. **异常处理**：在多个关键点捕获和处理异常，确保研究过程稳定

通过这个工作流程，ResearchConductor 能够自动化地进行黑洞研究，从多个来源收集信息，并生成全面的研究上下文。:-)

"""

"""
ResearchConductor 根据数据源类型执行不同的研究流程主要通过 `conduct_research` 方法中的条件分支实现。让我详细解析这个机制：

## 数据源类型判断与处理流程

ResearchConductor 通过检查 `researcher` 对象的属性来确定使用哪种数据源类型，然后执行相应的研究流程：

1. **提供的源 URL**：
   ```python
   if self.researcher.source_urls:
       # 使用提供的源URL进行研究
       research_data = await self._get_context_by_urls(self.researcher.source_urls)
       if self.researcher.complement_source_urls:
           # 如果需要补充源，则使用网络搜索
           additional_research = await self._get_context_by_web_search(...)
   ```
   - 首先检查 `source_urls` 是否存在
   - 调用 `_get_context_by_urls` 方法处理这些 URL
   - 如果设置了 `complement_source_urls`，还会进行补充网络搜索

2. **网络搜索**：
   ```python
   elif self.researcher.report_source == ReportSource.Web.value:
       # 使用网络搜索进行研究
       research_data = await self._get_context_by_web_search(...)
   ```
   - 当 `report_source` 设为 `Web` 时触发
   - 调用 `_get_context_by_web_search` 执行网络搜索

3. **本地文档搜索**：
   ```python
   elif self.researcher.report_source == ReportSource.Local.value:
       # 使用本地文档搜索进行研究
       document_data = await DocumentLoader(self.researcher.cfg.doc_path).load()
       if self.researcher.vector_store:
           self.researcher.vector_store.load(document_data)
       research_data = await self._get_context_by_web_search(...)
   ```
   - 当 `report_source` 设为 `Local` 时触发
   - 使用 `DocumentLoader` 加载本地文档
   - 如果配置了向量存储，将文档加载到向量存储
   - 使用相同的 `_get_context_by_web_search` 方法处理这些文档

4. **混合搜索**：
   ```python
   elif self.researcher.report_source == ReportSource.Hybrid.value:
       # 混合模式：结合本地文档和网络搜索
       if self.researcher.document_urls:
           document_data = await OnlineDocumentLoader(...).load()
       else:
           document_data = await DocumentLoader(...).load()
       docs_context = await self._get_context_by_web_search(...)
       web_context = await self._get_context_by_web_search(...)
       research_data = self.researcher.prompt_family.join_local_web_documents(...)
   ```
   - 当 `report_source` 设为 `Hybrid` 时触发
   - 从在线或本地来源加载文档
   - 分别处理文档上下文和网络上下文
   - 使用 `join_local_web_documents` 合并两种上下文

5. **Azure 存储**：
   ```python
   elif self.researcher.report_source == ReportSource.Azure.value:
       # 使用Azure存储中的文档进行研究
       azure_loader = AzureDocumentLoader(...)
       azure_files = await azure_loader.load()
       document_data = await DocumentLoader(azure_files).load()
       research_data = await self._get_context_by_web_search(...)
   ```
   - 当 `report_source` 设为 `Azure` 时触发
   - 使用 `AzureDocumentLoader` 从 Azure 存储加载文档

6. **LangChain 文档**：
   ```python
   elif self.researcher.report_source == ReportSource.LangChainDocuments.value:
       # 使用LangChain文档进行研究
       langchain_documents_data = await LangChainDocumentLoader(...).load()
       if self.researcher.vector_store:
           self.researcher.vector_store.load(langchain_documents_data)
       research_data = await self._get_context_by_web_search(...)
   ```
   - 当 `report_source` 设为 `LangChainDocuments` 时触发
   - 使用 `LangChainDocumentLoader` 加载文档

7. **LangChain 向量存储**：
   ```python
   elif self.researcher.report_source == ReportSource.LangChainVectorStore.value:
       # 使用LangChain向量存储进行研究
       research_data = await self._get_context_by_vectorstore(...)
   ```
   - 当 `report_source` 设为 `LangChainVectorStore` 时触发
   - 直接通过 `_get_context_by_vectorstore` 使用向量存储

## 核心处理方法

不同的数据源类型最终会调用三个主要方法之一：

1. **_get_context_by_urls**：
   - 处理指定的 URL
   - 抓取内容并加载到向量存储（如果配置）
   - 返回与查询相似的内容

2. **_get_context_by_web_search**：
   - 生成子查询
   - 并行处理每个子查询
   - 抓取内容、过滤并合并结果

3. **_get_context_by_vectorstore**：
   - 生成子查询
   - 使用向量存储搜索相关内容
   - 并行处理查询

## 实现机制

这种基于数据源类型的分流处理是通过以下机制实现的：

1. **枚举值定义**：使用 `ReportSource` 枚举定义不同数据源类型
2. **条件分支**：使用 if-elif 结构根据数据源类型选择处理流程
3. **多态处理方法**：不同的处理方法针对不同类型的数据源优化
4. **组合模式**：混合类型通过组合多个处理方法实现
5. **公共后处理**：所有流程最终会合并到统一的源整理和结果返回逻辑

这种设计使 ResearchConductor 能够灵活处理多种数据源，同时保持核心研究逻辑的一致性。根据研究任务需求，可以简单地切换数据源类型，而无需修改核心研究流程。:-)

"""

"""
这种基于数据源类型的分流处理是通过以下机制实现的：
枚举值定义：使用 ReportSource 枚举定义不同数据源类型
条件分支：使用 if-elif 结构根据数据源类型选择处理流程
多态处理方法：不同的处理方法针对不同类型的数据源优化
组合模式：混合类型通过组合多个处理方法实现
公共后处理：所有流程最终会合并到统一的源整理和结果返回逻辑
"""

class ResearchConductor:
    """管理和协调研究过程。"""

    def __init__(self, researcher):
        # 初始化研究协调器，设置研究者对象、日志和JSON处理器
        self.researcher = researcher
        self.logger = logging.getLogger('research')
        self.json_handler = get_json_handler()

    async def plan_research(self, query, query_domains=None):
        # 规划研究计划，根据查询和可选的查询领域
        self.logger.info(f"Planning research for query: {query}")
        if query_domains:
            self.logger.info(f"Query domains: {query_domains}")
        
        await stream_output(
            "logs",
            "planning_research",
            f"🌐 正在浏览网络以了解更多关于任务的信息: {query}...",
            self.researcher.websocket,
        )

        # 获取初步搜索结果
        search_results = await get_search_results(query, self.researcher.retrievers[0], query_domains)
        self.logger.info(f"Initial search results obtained: {len(search_results)} results")

        await stream_output(
            "logs",
            "planning_research",
            f"🤔 正在规划研究策略和子任务...",
            self.researcher.websocket,
        )

        # 生成研究大纲
        outline = await plan_research_outline(
            query=query,
            search_results=search_results,
            agent_role_prompt=self.researcher.role,
            cfg=self.researcher.cfg,
            parent_query=self.researcher.parent_query,
            report_type=self.researcher.report_type,
            cost_callback=self.researcher.add_costs,
        )
        self.logger.info(f"Research outline planned: {outline}")
        return outline

    async def conduct_research(self):
        """运行GPT Researcher来进行研究"""
        # 记录查询到JSON处理器
        if self.json_handler:
            self.json_handler.update_content("query", self.researcher.query)
        
        self.logger.info(f"Starting research for query: {self.researcher.query}")
        
        # 在每个研究任务开始时重置已访问URL和源URL
        self.researcher.visited_urls.clear()
        research_data = []

        if self.researcher.verbose:
            await stream_output(
                "logs",
                "starting_research",
                f"🔍 正在开始研究任务: '{self.researcher.query}'...",
                self.researcher.websocket,
            )
            await stream_output(
                "logs",
                "agent_generated",
                self.researcher.agent,
                self.researcher.websocket
            )

        # 根据下面的源类型研究相关源
        if self.researcher.source_urls:
            # 使用提供的源URL进行研究
            self.logger.info("Using provided source URLs")
            research_data = await self._get_context_by_urls(self.researcher.source_urls)
            if research_data and len(research_data) == 0 and self.researcher.verbose:
                await stream_output(
                    "logs",
                    "answering_from_memory",
                    f"🧐 我无法在提供的源中找到相关内容...",
                    self.researcher.websocket,
                )
            if self.researcher.complement_source_urls:
                # 如果需要补充源，则使用网络搜索
                self.logger.info("Complementing with web search")
                additional_research = await self._get_context_by_web_search(self.researcher.query, [], self.researcher.query_domains)
                research_data += ' '.join(additional_research)

        elif self.researcher.report_source == ReportSource.Web.value:
            # 使用网络搜索进行研究
            self.logger.info("Using web search")
            research_data = await self._get_context_by_web_search(self.researcher.query, [], self.researcher.query_domains)

        elif self.researcher.report_source == ReportSource.Local.value:
            # 使用本地文档搜索进行研究
            self.logger.info("Using local search")
            document_data = await DocumentLoader(self.researcher.cfg.doc_path).load()
            self.logger.info(f"Loaded {len(document_data)} documents")
            if self.researcher.vector_store:
                self.researcher.vector_store.load(document_data)

            research_data = await self._get_context_by_web_search(self.researcher.query, document_data, self.researcher.query_domains)

        # 混合搜索，包括本地文档和网络源
        elif self.researcher.report_source == ReportSource.Hybrid.value:
            # 混合模式：结合本地文档和网络搜索
            if self.researcher.document_urls:
                document_data = await OnlineDocumentLoader(self.researcher.document_urls).load()
            else:
                document_data = await DocumentLoader(self.researcher.cfg.doc_path).load()
            if self.researcher.vector_store:
                self.researcher.vector_store.load(document_data)
            docs_context = await self._get_context_by_web_search(self.researcher.query, document_data, self.researcher.query_domains)
            web_context = await self._get_context_by_web_search(self.researcher.query, [], self.researcher.query_domains)
            research_data = self.researcher.prompt_family.join_local_web_documents(docs_context, web_context)

        elif self.researcher.report_source == ReportSource.Azure.value:
            # 使用Azure存储中的文档进行研究
            from ..document.azure_document_loader import AzureDocumentLoader
            azure_loader = AzureDocumentLoader(
                container_name=os.getenv("AZURE_CONTAINER_NAME"),
                connection_string=os.getenv("AZURE_CONNECTION_STRING")
            )
            azure_files = await azure_loader.load()
            document_data = await DocumentLoader(azure_files).load()  # 复用现有加载器
            research_data = await self._get_context_by_web_search(self.researcher.query, document_data)
            
        elif self.researcher.report_source == ReportSource.LangChainDocuments.value:
            # 使用LangChain文档进行研究
            langchain_documents_data = await LangChainDocumentLoader(
                self.researcher.documents
            ).load()
            if self.researcher.vector_store:
                self.researcher.vector_store.load(langchain_documents_data)
            research_data = await self._get_context_by_web_search(
                self.researcher.query, langchain_documents_data, self.researcher.query_domains
            )

        elif self.researcher.report_source == ReportSource.LangChainVectorStore.value:
            # 使用LangChain向量存储进行研究
            research_data = await self._get_context_by_vectorstore(self.researcher.query, self.researcher.vector_store_filter)

        # 对源进行排名和整理
        self.researcher.context = research_data
        if self.researcher.cfg.curate_sources:
            self.logger.info("Curating sources")
            self.researcher.context = await self.researcher.source_curator.curate_sources(research_data)

        if self.researcher.verbose:
            await stream_output(
                "logs",
                "research_step_finalized",
                f"研究步骤已完成。\n💸 总研究成本: ${self.researcher.get_costs()}",
                self.researcher.websocket,
            )
            if self.json_handler:
                self.json_handler.update_content("costs", self.researcher.get_costs())
                self.json_handler.update_content("context", self.researcher.context)

        self.logger.info(f"Research completed. Context size: {len(str(self.researcher.context))}")
        return self.researcher.context

    async def _get_context_by_urls(self, urls):
        """抓取并压缩给定URL的上下文内容"""
        self.logger.info(f"Getting context from URLs: {urls}")
        
        # 获取新的未访问URL
        new_search_urls = await self._get_new_urls(urls)
        self.logger.info(f"New URLs to process: {new_search_urls}")

        # 抓取URL内容
        scraped_content = await self.researcher.scraper_manager.browse_urls(new_search_urls)
        self.logger.info(f"Scraped content from {len(scraped_content)} URLs")

        # 如果有向量存储，则加载内容到向量存储
        if self.researcher.vector_store:
            self.logger.info("Loading content into vector store")
            self.researcher.vector_store.load(scraped_content)

        # 获取与查询相似的内容
        context = await self.researcher.context_manager.get_similar_content_by_query(
            self.researcher.query, scraped_content
        )
        return context

    async def _get_context_by_vectorstore(self, query, filter: dict | None = None):
        """
        通过搜索向量存储为研究任务生成上下文
        返回:
            context: 上下文列表
        """
        self.logger.info(f"Starting vectorstore search for query: {query}")
        context = []
        # 生成子查询，包括原始查询
        sub_queries = await self.plan_research(query)
        # 如果这不是子研究的一部分，添加原始查询以获得更好的结果
        if self.researcher.report_type != "subtopic_report":
            sub_queries.append(query)

        if self.researcher.verbose:
            await stream_output(
                "logs",
                "subqueries",
                f"🗂️ 我将基于以下查询进行研究: {sub_queries}...",
                self.researcher.websocket,
                True,
                sub_queries,
            )

        # 使用asyncio.gather异步处理子查询
        context = await asyncio.gather(
            *[
                self._process_sub_query_with_vectorstore(sub_query, filter)
                for sub_query in sub_queries
            ]
        )
        return context

    async def _get_context_by_web_search(self, query, scraped_data: list | None = None, query_domains: list | None = None):
        """
        通过搜索查询并抓取结果为研究任务生成上下文
        返回:
            context: 上下文列表
        """
        self.logger.info(f"Starting web search for query: {query}")
        
        if scraped_data is None:
            scraped_data = []
        if query_domains is None:
            query_domains = []

        # 生成子查询，包括原始查询
        sub_queries = await self.plan_research(query, query_domains)
        self.logger.info(f"Generated sub-queries: {sub_queries}")
        
        # 如果这不是子研究的一部分，添加原始查询以获得更好的结果
        if self.researcher.report_type != "subtopic_report":
            sub_queries.append(query)

        if self.researcher.verbose:
            await stream_output(
                "logs",
                "subqueries",
                f"🗂️ 我将基于以下查询进行研究: {sub_queries}...",
                self.researcher.websocket,
                True,
                sub_queries,
            )

        # 使用asyncio.gather异步处理子查询
        try:
            context = await asyncio.gather(
                *[
                    self._process_sub_query(sub_query, scraped_data, query_domains)
                    for sub_query in sub_queries
                ]
            )
            self.logger.info(f"Gathered context from {len(context)} sub-queries")
            # 过滤掉空结果并连接上下文
            context = [c for c in context if c]
            if context:
                combined_context = " ".join(context)
                self.logger.info(f"Combined context size: {len(combined_context)}")
                return combined_context
            return []
        except Exception as e:
            self.logger.error(f"Error during web search: {e}", exc_info=True)
            return []

    async def _process_sub_query(self, sub_query: str, scraped_data: list = [], query_domains: list = []):
        """处理子查询，基于它抓取URL并收集上下文。"""
        if self.json_handler:
            self.json_handler.log_event("sub_query", {
                "query": sub_query,
                "scraped_data_size": len(scraped_data)
            })
        
        if self.researcher.verbose:
            await stream_output(
                "logs",
                "running_subquery_research",
                f"\n🔍 正在为 '{sub_query}' 进行研究...",
                self.researcher.websocket,
            )

        try:
            # 如果没有预先抓取的数据，则抓取数据
            if not scraped_data:
                scraped_data = await self._scrape_data_by_urls(sub_query, query_domains)
                self.logger.info(f"Scraped data size: {len(scraped_data)}")

            # 获取与子查询相似的内容
            content = await self.researcher.context_manager.get_similar_content_by_query(sub_query, scraped_data)
            self.logger.info(f"Content found for sub-query: {len(str(content)) if content else 0} chars")

            if not content and self.researcher.verbose:
                await stream_output(
                    "logs",
                    "subquery_context_not_found",
                    f"🤷 没有找到 '{sub_query}' 的相关内容...",
                    self.researcher.websocket,
                )
            if content:
                if self.json_handler:
                    self.json_handler.log_event("content_found", {
                        "sub_query": sub_query,
                        "content_size": len(content)
                    })
            return content
        except Exception as e:
            self.logger.error(f"Error processing sub-query {sub_query}: {e}", exc_info=True)
            return ""

    async def _process_sub_query_with_vectorstore(self, sub_query: str, filter: dict | None = None):
        """从用户提供的向量存储中获取子查询的上下文

        参数:
            sub_query (str): 从原始查询生成的子查询

        返回:
            str: 从搜索中收集的上下文
        """
        if self.researcher.verbose:
            await stream_output(
                "logs",
                "running_subquery_with_vectorstore_research",
                f"\n🔍 正在为 '{sub_query}' 进行研究...",
                self.researcher.websocket,
            )

        # 使用向量存储获取与查询相关的内容
        context = await self.researcher.context_manager.get_similar_content_by_query_with_vectorstore(sub_query, filter)

        return context

    async def _get_new_urls(self, url_set_input):
        """从给定的URL集合中获取新的URL。
        参数: url_set_input (set[str]): 要获取新URL的URL集合
        返回: list[str]: 从给定URL集合中的新URL
        """

        new_urls = []
        for url in url_set_input:
            # 检查URL是否已访问
            if url not in self.researcher.visited_urls:
                self.researcher.visited_urls.add(url)
                new_urls.append(url)
                if self.researcher.verbose:
                    await stream_output(
                        "logs",
                        "added_source_url",
                        f"✅ 添加研究源URL: {url}\n",
                        self.researcher.websocket,
                        True,
                        url,
                    )

        return new_urls

    """
        在 ResearchConductor 中执行网络搜索时，系统通过一系列机制解决了"去哪找"、"质量保证"、"充分性判断"和"搜索终止"的问题。让我详细分析这些关键机制：

## 1. 去哪找（信息源确定）

ResearchConductor 通过以下方式确定信息来源：

```python
async def _search_relevant_source_urls(self, query, query_domains: list | None = None):
    # 遍历所有检索器
    for retriever_class in self.researcher.retrievers:
        # 使用子查询实例化检索器
        retriever = retriever_class(query, query_domains=query_domains)
        
        # 使用当前检索器执行搜索
        search_results = await asyncio.to_thread(
            retriever.search, max_results=self.researcher.cfg.max_search_results_per_query
        )
        
        # 从搜索结果中收集新URL
        search_urls = [url.get("href") for url in search_results]
        new_search_urls.extend(search_urls)
```

关键机制：
- **多检索器策略**：使用 `self.researcher.retrievers` 中的多个检索器，可能包括不同的搜索引擎
- **域名限制**：支持 `query_domains` 参数，可以限制在特定域名范围内搜索
- **搜索结果上限**：通过 `max_results=self.researcher.cfg.max_search_results_per_query` 控制每次查询返回的结果数量
- **去重处理**：通过 `_get_new_urls` 方法确保不重复访问已处理过的 URL

## 2. 质量保证（内容筛选）

系统通过以下机制确保获取的信息质量：

```python
# 生成子查询，包括原始查询
sub_queries = await self.plan_research(query, query_domains)

# 获取与子查询相似的内容
content = await self.researcher.context_manager.get_similar_content_by_query(sub_query, scraped_data)
```

以及：

```python
# 对源进行排名和整理
self.researcher.context = research_data
if self.researcher.cfg.curate_sources:
    self.logger.info("Curating sources")
    self.researcher.context = await self.researcher.source_curator.curate_sources(research_data)
```

关键机制：
- **智能子查询生成**：通过 `plan_research` 方法生成有针对性的子查询，提高搜索精确度
- **语义相似度筛选**：使用 `get_similar_content_by_query` 方法基于语义相似度选择与查询相关的内容
- **源整理**：通过可选的 `curate_sources` 功能对收集的信息进行质量筛选和排序
- **URL随机化**：使用 `random.shuffle(new_search_urls)` 避免集中于单一来源的偏见

## 3. 充分性判断（信息完整性）

系统通过以下方式判断信息是否足够：

```python
# 生成子查询，包括原始查询
sub_queries = await self.plan_research(query, query_domains)
# 如果这不是子研究的一部分，添加原始查询以获得更好的结果
if self.researcher.report_type != "subtopic_report":
    sub_queries.append(query)
```

关键机制：
- **子查询覆盖**：通过生成多个子查询，确保覆盖主题的不同方面
- **原始查询补充**：在子查询列表中添加原始查询，确保基础信息被覆盖
- **内容量监控**：记录搜集内容的大小，如 `self.logger.info(f"Combined context size: {len(combined_context)}")`
- **多源汇总**：从多个来源收集信息，提高信息的完整性和多样性

## 4. 搜索终止（完成条件）

系统通过以下方式决定何时结束搜索：

```python
# 使用asyncio.gather异步处理子查询
context = await asyncio.gather(
    *[
        self._process_sub_query(sub_query, scraped_data, query_domains)
        for sub_query in sub_queries
    ]
)
```

关键机制：
- **并行处理终止**：所有子查询并行处理完成后自然终止搜索
- **预设结果数量**：通过 `max_results=self.researcher.cfg.max_search_results_per_query` 限制每个查询的结果数量
- **子查询清单完成**：当所有计划的子查询处理完毕，搜索过程自动结束
- **异常处理终止**：当出现错误时通过异常处理提前终止，如 `except Exception as e: self.logger.error(...)`

## 实际流程示例（搜索"黑洞"）

1. **开始搜索**：接收"黑洞"查询
2. **生成子查询**：通过 `plan_research` 生成如"黑洞形成"、"黑洞特性"等子查询
3. **并行处理**：同时处理所有子查询
   - 对每个子查询，使用多个检索器搜索相关 URL
   - 抓取 URL 内容
   - 筛选与子查询相关的内容
4. **内容合并**：合并所有子查询的结果，过滤空结果
5. **内容整理**：如果启用了 `curate_sources`，会对收集的信息进行质量排序
6. **返回结果**：返回整合后的研究内容

通过这种设计，ResearchConductor 能够有效地解决网络搜索中的关键挑战，既确保了信息的广度和深度，又保证了搜索过程的效率和自动终止。:-)

    """
    async def _search_relevant_source_urls(self, query, query_domains: list | None = None):
        """搜索与查询相关的源URL"""
        new_search_urls = []
        if query_domains is None:
            query_domains = []

        # 遍历所有检索器
        for retriever_class in self.researcher.retrievers:
            # 使用子查询实例化检索器
            retriever = retriever_class(query, query_domains=query_domains)

            # 使用当前检索器执行搜索
            search_results = await asyncio.to_thread(
                retriever.search, max_results=self.researcher.cfg.max_search_results_per_query
            )

            # 从搜索结果中收集新URL
            search_urls = [url.get("href") for url in search_results]
            new_search_urls.extend(search_urls)

        # 获取唯一URL
        new_search_urls = await self._get_new_urls(new_search_urls)
        random.shuffle(new_search_urls)

        return new_search_urls

    async def _scrape_data_by_urls(self, sub_query, query_domains: list | None = None):
        """
        在多个检索器中运行子查询并抓取结果URL。

        参数:
            sub_query (str): 要搜索的子查询。

        返回:
            list: 抓取内容结果列表。
        """
        if query_domains is None:
            query_domains = []

        # 搜索相关的源URL
        new_search_urls = await self._search_relevant_source_urls(sub_query, query_domains)

        # 如果verbose模式开启，记录研究过程
        if self.researcher.verbose:
            await stream_output(
                "logs",
                "researching",
                f"🤔 正在跨多个来源研究相关信息...\n",
                self.researcher.websocket,
            )

        # 抓取新URL
        scraped_content = await self.researcher.scraper_manager.browse_urls(new_search_urls)

        # 如果有向量存储，加载抓取的内容
        if self.researcher.vector_store:
            self.researcher.vector_store.load(scraped_content)

        return scraped_content
